/*
  +----------------------------------------------------------------------+
  | PHP Version 5                                                        |
  +----------------------------------------------------------------------+
  | Copyright (c) 1997-2007 The PHP Group                                |
  +----------------------------------------------------------------------+
  | This source file is subject to version 3.0 of the PHP license,       |
  | that is bundled with this package in the file LICENSE, and is        |
  | available through the world-wide-web at the following url:           |
  | http://www.php.net/license/3_0.txt.                                  |
  | If you did not receive a copy of the PHP license and are unable to   |
  | obtain it through the world-wide-web, please send a note to          |
  | license@php.net so we can mail you a copy immediately.               |
  +----------------------------------------------------------------------+
  | Authors: Wang Wenlin <wl AT soplwang DOT com>                        |
  +----------------------------------------------------------------------+
*/

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include "php.h"
#include <stdio.h>

#include "ext/standard/php_string.h"
#include "ext/standard/php_var.h"
#include "ext/standard/php_smart_str.h"
#include "php_badwords.h"

ZEND_DECLARE_MODULE_GLOBALS(badwords)

#define BW_WORDS_MIN_LEN  2
#define BW_WORDS_MAX_LEN  2047

#define BW_CBITS_SET(bits, c, c2) \
	((bits)[(unsigned char)(c)] |= (1 << ((unsigned char)(c2) & 7)))

#define BW_CBITS_TEST(bits, c, c2) \
	((bits)[(unsigned char)(c)] & (1 << ((unsigned char)(c2) & 7)))

#define BW_LBITS_SET(bits, len) \
	((bits)[(unsigned char)(len)] |= (1 << ((unsigned char)((len) >> 8) & 7)))

#define BW_LBITS_TEST(bits, len) \
	((bits)[(unsigned char)(len)] & (1 << ((unsigned char)((len) >> 8) & 7)))

struct _Badwords_cache {
	HashTable *hash;
	HashTable  tmp_hash;
	int case_insensitive;
	int minlen;
	int maxlen;
	unsigned char cbits[256];
	unsigned char lbits[256];
};

/* {{{ badwords_functions[]
 */
zend_function_entry badwords_functions[] = {
	PHP_FE(badwords_filter,		NULL)
	PHP_FE(badwords_ifilter,	NULL)
	PHP_FE(badwords_detect,		NULL)
	PHP_FE(badwords_idetect,	NULL)
	PHP_FE(badwords_find,		NULL)
	PHP_FE(badwords_ifind,		NULL)
	{NULL, NULL, NULL}
};
/* }}} */

/* {{{ badwords_module_entry
 */
zend_module_entry badwords_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
	STANDARD_MODULE_HEADER,
#endif
	"badwords",
	badwords_functions,
	PHP_MINIT(badwords),
	NULL,
	PHP_RINIT(badwords),
	NULL,
	NULL,
#if ZEND_MODULE_API_NO >= 20010901
	PHP_BADWORDS_VERSION,
#endif
	STANDARD_MODULE_PROPERTIES
};
/* }}} */

#ifdef COMPILE_DL_BADWORDS
ZEND_GET_MODULE(badwords)
#endif

/* {{{ php_badwords_init_globals()
*/
static void php_badwords_init_globals(zend_badwords_globals *badwords_globals_p TSRMLS_DC)
{
	BADWORDS_G(_badwords_cache)  = NULL;
}
/* }}} */

/* {{{ PHP_MINIT_FUNCTION
 */
PHP_MINIT_FUNCTION(badwords)
{
#ifdef ZTS
	ts_allocate_id(&badwords_globals_id, sizeof(zend_badwords_globals), (ts_allocate_ctor) php_badwords_init_globals, NULL);
#else
	php_badwords_init_globals(&badwords_globals TSRMLS_CC);
#endif
	return SUCCESS;
}
/* }}} */

/* {{{ PHP_RINIT_FUNCTION
 */
PHP_RINIT_FUNCTION(badwords)
{
	BADWORDS_G(_badwords_cache)  = NULL;
	return SUCCESS;
}
/* }}} */

/* ----------------
   module functions
   ---------------- */
   
/* {{{ php_badwords_get_cache
 */
static struct _Badwords_cache* php_badwords_get_cache(HashTable *hash, int case_insensitive)
{
	char  *string_key;
	uint   string_key_len;
	ulong  num_key;
	zval **entry;
	int    len;
	char  *key;
	zval   ctmp;
	HashPosition hpos;

	register struct _Badwords_cache
		*cache = (struct _Badwords_cache *) BADWORDS_G(_badwords_cache);

	if (!cache || cache->hash != hash || cache->case_insensitive != case_insensitive) {
		if (!cache) {
			BADWORDS_G(_badwords_cache) = cache =
				(struct _Badwords_cache *) emalloc(sizeof(struct _Badwords_cache));
		} else if (cache->hash) {
			zend_hash_destroy(&cache->tmp_hash);
		}

		memset(cache, 0, sizeof(struct _Badwords_cache));
		cache->hash = hash;
		cache->case_insensitive = case_insensitive;
		cache->minlen = 128*1024;
		cache->maxlen = 0;

		zend_hash_init(&cache->tmp_hash, zend_hash_num_elements(hash), NULL, NULL, 0);
		zend_hash_internal_pointer_reset_ex(hash, &hpos);
		while (zend_hash_get_current_data_ex(hash, (void **)&entry, &hpos) == SUCCESS) {
			switch (zend_hash_get_current_key_ex(hash, &string_key, &string_key_len, &num_key, 0, &hpos)) {
				case HASH_KEY_IS_STRING:
					len = string_key_len-1;
					key = string_key;
					if (BW_WORDS_MIN_LEN <= len && len <= BW_WORDS_MAX_LEN) {
						if (case_insensitive) {
							key = estrndup(key, len);
							php_strtolower(key, len);
						}
						zend_hash_add(&cache->tmp_hash, key, string_key_len, entry, sizeof(zval*), NULL);
						BW_LBITS_SET(cache->lbits, len);
						BW_CBITS_SET(cache->cbits, key[0], key[1]);
						if (case_insensitive)
							efree(key);
						if (len > cache->maxlen)
							cache->maxlen = len;
						if (len < cache->minlen)
							cache->minlen = len;
					}
					break;

				case HASH_KEY_IS_LONG:
					Z_TYPE(ctmp) = IS_LONG;
					Z_LVAL(ctmp) = num_key;
					convert_to_string(&ctmp);
					len = Z_STRLEN(ctmp);
					key = Z_STRVAL(ctmp);
					if (BW_WORDS_MIN_LEN <= len && len <= BW_WORDS_MAX_LEN) {
						if (case_insensitive)
							php_strtolower(key, len);
						zend_hash_add(&cache->tmp_hash, key, len+1, entry, sizeof(zval*), NULL);
						BW_LBITS_SET(cache->lbits, len);
						BW_CBITS_SET(cache->cbits, key[0], key[1]);
						if (len > cache->maxlen)
							cache->maxlen = len;
						if (len < cache->minlen)
							cache->minlen = len;
					}
					zval_dtor(&ctmp);
					break;
			}
			zend_hash_move_forward_ex(hash, &hpos);
		}
	}
	
	return cache;
}
/* }}} */

/* {{{ php_badwords_filter
 */
static void php_badwords_filter(zval *return_value, char *str, int slen, HashTable *hash)
{
	int pos, len, found;
	int minlen, maxlen;
	char *key;
	zval **entry;
	smart_str result = {0};

	register struct _Badwords_cache
		*cache = php_badwords_get_cache(hash, 0);

	minlen = cache->minlen;
	maxlen = cache->maxlen;
	key = emalloc(maxlen + 1);

	for (found = 0, pos = 0; pos < slen; found = 0) {
		if ((pos + maxlen) > slen) {
			maxlen = slen - pos;
		}
		
		if (maxlen >= minlen && BW_CBITS_TEST(cache->cbits, str[pos], str[pos+1])) {
			memcpy(key, str+pos, maxlen);

			for (len = maxlen; len >= minlen; len--) {
				if (!BW_LBITS_TEST(cache->lbits, len))
					continue;

				key[len] = 0;
				
				if (zend_hash_find(&cache->tmp_hash, key, len+1, (void**)&entry) == SUCCESS) {
					char *tval;
					int tlen;
					zval tmp;

					if (Z_TYPE_PP(entry) != IS_STRING) {
						tmp = **entry;
						zval_copy_ctor(&tmp);
						convert_to_string(&tmp);
						tlen = Z_STRLEN(tmp);
						tval = Z_STRVAL(tmp);
					} else {
						tlen = Z_STRLEN_PP(entry);
						tval = Z_STRVAL_PP(entry);
					}

					smart_str_appendl(&result, tval, tlen);
					pos += len;
					found = 1;

					if (Z_TYPE_PP(entry) != IS_STRING) {
						zval_dtor(&tmp);
					}
					break;
				}
			}
		}

		if (!found) {
			smart_str_appendc(&result, str[pos++]);
		}
	}

	efree(key);
	smart_str_0(&result);

	RETVAL_STRINGL(result.c, result.len, 0);
}
/* }}} */

/* {{{ php_badwords_ifilter
 */
static void php_badwords_ifilter(zval *return_value, char *str, int slen, HashTable *hash)
{
	int pos, len, found;
	int minlen, maxlen;
	char *key;
	zval **entry;
	smart_str result = {0};
	char *dupstr;

	register struct _Badwords_cache
		*cache = php_badwords_get_cache(hash, 1);

	dupstr = estrndup(str, slen);
	php_strtolower(dupstr, slen);

	minlen = cache->minlen;
	maxlen = cache->maxlen;
	key = emalloc(maxlen + 1);

	for (found = 0, pos = 0; pos < slen; found = 0) {
		if ((pos + maxlen) > slen) {
			maxlen = slen - pos;
		}
		
		if (maxlen >= minlen && BW_CBITS_TEST(cache->cbits, dupstr[pos], dupstr[pos+1])) {
			memcpy(key, dupstr+pos, maxlen);

			for (len = maxlen; len >= minlen; len--) {
				if (!BW_LBITS_TEST(cache->lbits, len))
					continue;

				key[len] = 0;
				
				if (zend_hash_find(&cache->tmp_hash, key, len+1, (void**)&entry) == SUCCESS) {
					char *tval;
					int tlen;
					zval tmp;

					if (Z_TYPE_PP(entry) != IS_STRING) {
						tmp = **entry;
						zval_copy_ctor(&tmp);
						convert_to_string(&tmp);
						tlen = Z_STRLEN(tmp);
						tval = Z_STRVAL(tmp);
					} else {
						tlen = Z_STRLEN_PP(entry);
						tval = Z_STRVAL_PP(entry);
					}

					smart_str_appendl(&result, tval, tlen);
					pos += len;
					found = 1;

					if (Z_TYPE_PP(entry) != IS_STRING) {
						zval_dtor(&tmp);
					}
					break;
				}
			}
		}

		if (!found) {
			smart_str_appendc(&result, str[pos++]);
		}
	}

	efree(key);
	efree(dupstr);
	smart_str_0(&result);

	RETVAL_STRINGL(result.c, result.len, 0);
}
/* }}} */

/* {{{ php_badwords_detect
 */
static void php_badwords_detect(zval *return_value, char *str, int slen, HashTable *hash)
{
	int pos, len, found;
	int minlen, maxlen;
	char *key;
	zval **entry;
	smart_str result = {0};

	register struct _Badwords_cache
		*cache = php_badwords_get_cache(hash, 0);

	minlen = cache->minlen;
	maxlen = cache->maxlen;
	key = emalloc(maxlen + 1);

	for (found = 0, pos = 0; !found && pos < slen; ++ pos) {
		if ((pos + maxlen) > slen) {
			maxlen = slen - pos;
		}

		if (maxlen >= minlen && BW_CBITS_TEST(cache->cbits, str[pos], str[pos+1])) {
			memcpy(key, str+pos, maxlen);

			for (len = maxlen; len >= minlen; len--) {
				if (!BW_LBITS_TEST(cache->lbits, len))
					continue;

				key[len] = 0;
				
				if (zend_hash_find(&cache->tmp_hash, key, len+1, (void**)&entry) == SUCCESS) {
					smart_str_appendl(&result, key, len);
					smart_str_0(&result);
					found = 1;
					break;
				}
			}
		}
	}

	efree(key);

	if (found)
		RETVAL_STRINGL(result.c, result.len, 0)
	else
		RETURN_EMPTY_STRING()
}
/* }}} */

/* {{{ php_badwords_idetect
 */
static void php_badwords_idetect(zval *return_value, char *str, int slen, HashTable *hash)
{
	int pos, len, found;
	int minlen, maxlen;
	char *key;
	zval **entry;
	smart_str result = {0};
	char *dupstr;

	register struct _Badwords_cache
		*cache = php_badwords_get_cache(hash, 1);

	dupstr = estrndup(str, slen);
	php_strtolower(dupstr, slen);

	minlen = cache->minlen;
	maxlen = cache->maxlen;
	key = emalloc(maxlen + 1);

	for (found = 0, pos = 0; !found && pos < slen; ++ pos) {
		if ((pos + maxlen) > slen) {
			maxlen = slen - pos;
		}

		if (maxlen >= minlen && BW_CBITS_TEST(cache->cbits, dupstr[pos], dupstr[pos+1])) {
			memcpy(key, dupstr+pos, maxlen);

			for (len = maxlen; len >= minlen; len--) {
				if (!BW_LBITS_TEST(cache->lbits, len))
					continue;

				key[len] = 0;
				
				if (zend_hash_find(&cache->tmp_hash, key, len+1, (void**)&entry) == SUCCESS) {
					smart_str_appendl(&result, str+pos, len);
					smart_str_0(&result);
					found = 1;
					break;
				}
			}
		}
	}

	efree(key);
	efree(dupstr);

	if (found)
		RETVAL_STRINGL(result.c, result.len, 0)
	else
		RETURN_EMPTY_STRING()
}
/* }}} */

/* {{{ proto string badwords_filter(string str, array tbl)
   Filter characters in str using given filter dictionary */
PHP_FUNCTION(badwords_filter)
{
	zval *tbl;
	char *str;
	int str_len;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sa", &str, &str_len, &tbl) == FAILURE) {
		return;
	}

	/* shortcut for empty string */
	if (str_len == 0) {
		RETURN_EMPTY_STRING();
	}

	php_badwords_filter(return_value, str, str_len, HASH_OF(tbl));
}
/* }}} */

/* {{{ proto string badwords_filter(string str, array tbl)
   Filter characters in str case insensitive using given filter dictionary */
PHP_FUNCTION(badwords_ifilter)
{
	zval *tbl;
	char *str;
	int str_len;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sa", &str, &str_len, &tbl) == FAILURE) {
		return;
	}

	/* shortcut for empty string */
	if (str_len == 0) {
		RETURN_EMPTY_STRING();
	}

	php_badwords_ifilter(return_value, str, str_len, HASH_OF(tbl));
}
/* }}} */

/* {{{ proto string badwords_detect(string str, array tbl)
   Detect characters in str using given filter dictionary */
PHP_FUNCTION(badwords_detect)
{
	zval *tbl;
	char *str;
	int str_len;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sa", &str, &str_len, &tbl) == FAILURE) {
		return;
	}

	/* shortcut for empty string */
	if (str_len == 0) {
		RETURN_EMPTY_STRING();
	}

	php_badwords_detect(return_value, str, str_len, HASH_OF(tbl));
}
/* }}} */

/* {{{ proto string badwords_idetect(string str, array tbl)
   Detect characters in str case insensitive using given filter dictionary */
PHP_FUNCTION(badwords_idetect)
{
	zval *tbl;
	char *str;
	int str_len;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sa", &str, &str_len, &tbl) == FAILURE) {
		return;
	}

	/* shortcut for empty string */
	if (str_len == 0) {
		RETURN_EMPTY_STRING();
	}

	php_badwords_idetect(return_value, str, str_len, HASH_OF(tbl));
}
/* }}} */

/* {{{ php_badwords_find
 */
static void php_badwords_find(zval *return_value, char *str, int slen, HashTable *hash)
{
	zval **entry;
	HashPosition hpos;
	int found = 0;
	smart_str result = {0};

	zend_hash_internal_pointer_reset_ex(hash, &hpos);
	while (zend_hash_get_current_data_ex(hash, (void **)&entry, &hpos) == SUCCESS) {
		char *tval;
		int tlen;
		zval tmp;

		if (Z_TYPE_PP(entry) != IS_STRING) {
			tmp = **entry;
			zval_copy_ctor(&tmp);
			convert_to_string(&tmp);
			tlen = Z_STRLEN(tmp);
			tval = Z_STRVAL(tmp);
		} else {
			tlen = Z_STRLEN_PP(entry);
			tval = Z_STRVAL_PP(entry);
		}
		
		if (BW_WORDS_MIN_LEN <= tlen && tlen <= BW_WORDS_MAX_LEN) {
			if (php_memnstr(str, tval, tlen, str+slen)) {
				smart_str_appendl(&result, tval, tlen);
				smart_str_0(&result);
				found = 1;
			}
		}

		if (Z_TYPE_PP(entry) != IS_STRING) {
			zval_dtor(&tmp);
		}

		if (found)
			break;
		else
			zend_hash_move_forward_ex(hash, &hpos);
	}

	if (found)
		RETVAL_STRINGL(result.c, result.len, 0)
	else
		RETURN_EMPTY_STRING();
}
/* }}} */

/* {{{ php_badwords_ifind
 */
static void php_badwords_ifind(zval *return_value, char *str, int slen, HashTable *hash)
{
	zval **entry;
	HashPosition hpos;
	int found = 0;
	smart_str result = {0};
	char *dupstr;

	dupstr = estrndup(str, slen);
	php_strtolower(dupstr, slen);

	zend_hash_internal_pointer_reset_ex(hash, &hpos);
	while (zend_hash_get_current_data_ex(hash, (void **)&entry, &hpos) == SUCCESS) {
		char *tval;
		int tlen;
		zval tmp;

		if (Z_TYPE_PP(entry) != IS_STRING) {
			tmp = **entry;
			zval_copy_ctor(&tmp);
			convert_to_string(&tmp);
			tlen = Z_STRLEN(tmp);
			tval = Z_STRVAL(tmp);
		} else {
			tlen = Z_STRLEN_PP(entry);
			tval = estrndup(Z_STRVAL_PP(entry), tlen);
		}

		php_strtolower(tval, tlen);
		
		if (BW_WORDS_MIN_LEN <= tlen && tlen <= BW_WORDS_MAX_LEN) {
			char *ptr = php_memnstr(dupstr, tval, tlen, dupstr+slen);
			if (ptr) {
				smart_str_appendl(&result, str+(ptr-dupstr), tlen);
				smart_str_0(&result);
				found = 1;
			}
		}

		if (Z_TYPE_PP(entry) != IS_STRING) {
			zval_dtor(&tmp);
		} else {
			efree(tval);
		}

		if (found)
			break;
		else
			zend_hash_move_forward_ex(hash, &hpos);
	}

	efree(dupstr);

	if (found)
		RETVAL_STRINGL(result.c, result.len, 0)
	else
		RETURN_EMPTY_STRING()
}
/* }}} */

/* {{{ proto string badwords_find(string str, array tbl)
   Find characters in str using given dictionary */
PHP_FUNCTION(badwords_find)
{
	zval *tbl;
	char *str;
	int str_len;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sa", &str, &str_len, &tbl) == FAILURE) {
		return;
	}

	/* shortcut for empty string */
	if (str_len == 0) {
		RETURN_EMPTY_STRING();
	}

	php_badwords_find(return_value, str, str_len, HASH_OF(tbl));
}
/* }}} */

/* {{{ proto string badwords_ifind(string str, array tbl)
   Find characters in str case insensitive using given dictionary */
PHP_FUNCTION(badwords_ifind)
{
	zval *tbl;
	char *str;
	int str_len;

	if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "sa", &str, &str_len, &tbl) == FAILURE) {
		return;
	}

	/* shortcut for empty string */
	if (str_len == 0) {
		RETURN_EMPTY_STRING();
	}

	php_badwords_ifind(return_value, str, str_len, HASH_OF(tbl));
}
/* }}} */

/*
 * Local variables:
 * tab-width: 4
 * c-basic-offset: 4
 * End:
 * vim600: noet sw=4 ts=4 fdm=marker
 * vim<600: noet sw=4 ts=4
 */
